#!/opt/python2.7/bin/python
##
#    Mail Check
#         
#    This class checks emails and performs actions based on them.
#       This will be missing a whole bunch of dependencies on google code, but may be of some use to someone
#    
#    @author     David Lasley, dave -at- dlasley -dot- net
#    @package    pyhp-imap
#    @version    $Id: mail_check.py 2 2012-12-12 19:35:18Z dave@dlasley.net $
import xml.dom.minidom, re, datetime, time, subprocess, sys, os
import wins.wins_core.exchange as exchange
from wins.wins_core.wins_exception import wins_exception

class mail_check(exchange.exchange):
    '''
        Mail check class
    '''
    xml_file = '/data01/xml_conf/mail_check.xml'
    backup_dir = '/data01/email_archive/'
    backup_xml = 'teamReporting_index.xml'
    backup_interval = 86400/2 #seconds
    fatal_error_net = 'Fatal Error Net'
    __component__ = 'mail_check'
    def __init__(self):
        '''
            Constructor. Init exchange
        '''
        exchange.exchange.__init__(self,verbose=False)

    def im_a_daemon(self):
        '''
            Infinite loop to create daemon. Loop will execute no more than once per minute. Backs up at self.backup_interval (seconds)
        '''
        last_backup = datetime.datetime.now()
        while 1:
            start = datetime.datetime.now()
            try:
                self.switch_mailbox('INBOX')
                self.mail_conf = self._parse_mail_conf_()
                self.scan_mailboxes()
                self.expunge_conn()
                now = datetime.datetime.now()
                #@todo   Implement forking for backup, problem was termination of imap connection on child _exit(0)
                #if last_backup is None:
                if (now-last_backup).seconds > self.backup_interval:
                    self.email_backup()
                    last_backup = now
                self.del_temp_files()
            except:
                raise wins_exception('Fatal issue',
                             component=self.__component__,
                             message='%s' % repr(sys.exc_info()))
            run_time = now - start
            sleep_for = 60-run_time.seconds
            if sleep_for>0:
                time.sleep(sleep_for)
    
    def email_backup(self):
        '''
            Email archiving solution
        '''
        import hashlib, gzip
        backup_dom = xml.dom.minidom.Document()
        try:
            backup_dom = xml.dom.minidom.parse('%s%s%s'%(self.backup_dir,os.path.sep,self.backup_xml))
            backup_root = backup_dom.getElementsByTagName('email_backup_index').item(0)
        except:
            backup_root = backup_dom.createElement('email_backup_index')
            backup_dom.appendChild(backup_root)
        mailboxes = self.get_mailboxes('INBOX')
        try:
            for mailbox in mailboxes:
                if not os.path.isdir(os.path.join(self.backup_dir,mailbox)):
                    try:
                        os.mkdir(os.path.join(self.backup_dir,mailbox))
                    except:
                        # subdirs
                        workingDir = mailbox.split(mailboxes[mailbox]['delimiter'])
                        workingStr = ''
                        for i in workingDir:
                            workingStr = os.path.join(workingStr, i)
                            if not os.path.isdir(os.path.join(self.backup_dir, workingStr)):
                                os.mkdir(os.path.join(self.backup_dir, workingStr))
                self.switch_mailbox(mailbox)
                msg_ids = self.message_search()
                for msg_id in msg_ids:
                    msg_datas = self.fetch(msg_id,['whole'])
                    try:
                        for i in msg_datas:
                            msg_data = msg_datas[i]
                            for response_part in msg_data:
                                if isinstance(response_part, tuple):
                                    message_el = backup_dom.createElement('email')
                                    msg = exchange.message_from_string(response_part[1])
                                    for header in [ 'subject', 'to', 'from', 'date']:
                                        message_el.setAttribute(header.upper(), msg[header].strip())
                                    hashed_message = hashlib.sha1(response_part[1]).hexdigest()
                                    message_el.setAttribute('Filename',"%s.gz" % hashed_message)
                                    message_el.setAttribute('Path',"%s" % mailbox)
                                    output = gzip.open(os.path.join(self.backup_dir, mailbox,'%s.gz'%hashed_message), 'wb')
                                    output.write(response_part[1])
                                    backup_root.appendChild(message_el)
                                    self.delete_message(msg_id)
                                    if self.verbose:
                                        print "Writing %s - %s" % (msg_id, hashed_message)
                                        print "Deleting %s" % (msg_id)
                    except:
                        raise wins_exception('Fatal Error in mail_check.email_backup()',
                                             component=self.__component__,
                                             message='%s\r%s' % (msg_id,repr(sys.exc_info())))
            self.expunge_conn()
        except TypeError:
            pass    #bool fix re #291, #327, #353, #361, #377, #362
        try:
            fh = open(os.path.join(self.backup_dir, self.backup_xml), 'w')
        except OSError as e:
            raise wins_exception('Error opening backup XML',
                                         component=self.__component__,
                                         message='%s\r%s' % (os.path.join(self.backup_dir,self.backup_xml),e))
        backup_dom.writexml(fh)
        fh.close()
    
    def scan_mailboxes(self):
        '''
            Parse emails in mailbox, send to match_email for processing
        '''
        header_fields = ['From','Subject','To','Date']
        msg_ids = self.message_search()
        for msg_id in msg_ids:
            try:
                int(msg_id)
                msg_datas = self.fetch(msg_id,['whole'])
                for i in msg_datas:
                    message = exchange.message_from_string(msg_datas[i][0][1])
                    processed_msg = {}
                    for j in header_fields:
                        processed_msg[j] = message.get(j)
                    processed_msg['msg'] = self._remove_html_tags(self.extract_body(message))
                    processed_msg['whole'] = msg_datas[i][0][1]
                    processed_msg['id'] = msg_id
                    self.match_email(processed_msg)
            except ValueError:
                if self.verbose: print 'Msg ID not int (%s). Passing.' % msg_id
    
    def match_email(self,email_information=None):
        '''
            Match email to actions
            
            @param  Dict    email_information   Email information required-['whole','id','From','Subject','msg']
        '''
        for email_conditions in self.mail_conf['email']:
            subject_match = False
            sender_match = False
            if len(email_conditions['search_functions']['subject']) > 0:
                for subject_condition in email_conditions['search_functions']['subject']:
                    p = re.compile('(%s)'%subject_condition['value'],re.IGNORECASE)
                    matches = p.findall(email_information['Subject'])
                    if len(matches) > 0:
                        subject_match = matches[0]
                        break
            else:
                subject_match = email_information['Subject']
            if len(email_conditions['search_functions']['from']) > 0:
                for sender_condition in email_conditions['search_functions']['from']:
                    p = re.compile('<(.*?)>')
                    sender = p.findall(email_information['From'])
                    from_regex = re.compile(sender_condition['value'], re.IGNORECASE)
                    if from_regex.search(sender[0]):
                        sender_match = sender[0]
                        email_information['sender'] = sender_match
                        break
            else:
                p = re.compile('<(.*?)>')
                sender = p.findall(email_information['From'])
                email_information['sender'] = sender[0]
                sender_match = sender
            if sender_match and subject_match:
                print 'match %s %s %s' %(email_information['Subject'],sender_match , subject_match)
                self._do_email_function(email_information,email_conditions)
                break
        else:
            print 'nomatch %s - %s - %s - %s' %(email_information['Subject'],sender[0],sender_match , subject_match)
            self._do_email_function(email_information,self.mail_conf['catch_all'][0])
    
    def _do_email_function(self,email_information,email_conditions):
        '''
            Perform email actions
            
            @param  Dict    email_information   Email information required-['whole','id','From','Subject','msg']
            @param  Dict    email_conditions    Email actions/conditions
        '''
        regex_results = {}
        email_parts_map = {'subject':'Subject','from':'sender','body':'msg'}
        email_parts_regex = re.compile('<!<([a-z]{1,})([0-9]{1,2})?>!>')
        regex_scraper = re.compile('<:<([a-zA-Z]{1,})([0-9]{1,2})>:>')
        for regex in email_conditions['search_functions']['regex']:
            p = re.compile(regex['value'], re.IGNORECASE)
            regex_results[regex['id']] = p.finditer(email_information['msg'])
            print regex_results[regex['id']]
        if email_conditions['actions']['cmd'] is not None:
            if len(email_conditions['search_functions']['attachment']) > 0:
                email_attachments = {}
                attachments = self.get_attachments(email_information['whole'])
                for att_condition in email_conditions['search_functions']['attachment']:
                    p = re.compile(att_condition['name'])
                    for attachment in attachments:
                        results = p.search(attachment)
                        if results is not None:
                            try:
                                att_id = att_condition['id']
                            except KeyError:
                                att_id = len(email_attachments)
                            email_attachments[att_id] = attachment
                print email_attachments
            cmd = email_conditions['actions']['cmd']
            start = 0
            new_cmd = []
            for match_part in email_parts_regex.finditer(cmd):
                new_cmd.append(cmd[start:match_part.start()])
                try:
                    if match_part.group(1) == 'attachment':
                        new_cmd.append(email_attachments[match_part.group(2).decode('ascii', 'replace')])
                    else:
                        new_cmd.append(email_information[email_parts_map[match_part.group(1)]].strip().decode('ascii','replace'))
                except KeyError as e:
                    self.move_message(email_information['id'],self.fatal_error_net)
                    raise wins_exception('Command regex error',
                             component=self.__component__,
                             message='%s' % e)
                start = match_part.end()
            new_cmd.append(cmd[start:])
            cmd = ''.join(new_cmd)
            new_cmd = []
            start = 0
            for match_part in regex_scraper.finditer(cmd):
                new_cmd.append(cmd[start:match_part.start()])
                regex_cnt = 0
                for result in regex_results[match_part.group(1)]:
                    group = int(match_part.group(2))
                    add_str = ',' if regex_cnt > 0 else ''
                    new_cmd.append('%s%s'%(add_str,result.group(group)))
                    regex_cnt+=1
                start = match_part.end()
            new_cmd.append(cmd[start:])
            cmd = ''.join(new_cmd)
            print cmd
            subprocess.call([cmd],shell=True)
        if email_conditions['actions']['delete'] == '1':
            self.delete_message(email_information['id'])
        elif email_conditions['actions']['move_to'] is not None:
            self.move_message(email_information['id'],'"Inbox/%s"'%email_conditions['actions']['move_to'])
            
    def _remove_html_tags(self,html):
        '''
            Remove HTML tags
            
            @param  String  html    String to sanitize
            
            @return String  Sanitized
        '''
        try:
            self.html_strip_regexes
        except:
            regexes = ['<[\/\!]*?[^<>]*?>','<script[^>]*?>.*?</script>','<![\s\S]*?--[||]*>','<style>.*?</style>','&nbsp;','[.a-z].*?\{.*?\}','^\s*$']
            self.html_strip_regexes = []
            for regex in regexes:
                self.html_strip_regexes.append(re.compile(regex))
        for regex in self.html_strip_regexes:
            html = regex.sub('',html)
        return html
    
    def _parse_mail_conf_(self):
        '''
            Load + parse XML
            
            @return Dict    Mail configurations dictionary
        '''
        mail_confs = {}
        mail_dom = xml.dom.minidom.parse(mail_check.xml_file)
        types = ['email','catch_all']
        for typ in types:
            mail_conf = []
            for email_el in mail_dom.getElementsByTagName(typ):
                actions = {'delete':None,'move_to':None,'cmd':None}
                search_functions = {'subject':[],'from':[],'attachment':[],'regex':[]}
                for child_el in email_el.childNodes:
                    if child_el.nodeType == xml.dom.Node.ELEMENT_NODE:
                        try:
                            actions[child_el.nodeName]
                            actions[child_el.nodeName] = child_el.firstChild.nodeValue
                        except KeyError:
                            this_function = {}
                            for (attr_name, attr_value) in child_el.attributes.items():
                                this_function[attr_name] = attr_value
                            try:
                                this_function['value'] = child_el.firstChild.nodeValue
                            except:
                                pass
                            search_functions[child_el.nodeName].append(this_function)
                mail_conf.append({'actions':actions,'search_functions':search_functions})
            mail_confs[typ] = mail_conf
        return mail_confs
    
if __name__ == '__main__':
    mail = mail_check()
    mail.im_a_daemon()
